/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.thinkgem.jeesite.modules.act.rest.diagram.services;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.activiti.engine.ActivitiException;
import org.activiti.engine.ActivitiObjectNotFoundException;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.impl.bpmn.behavior.BoundaryEventActivityBehavior;
import org.activiti.engine.impl.bpmn.behavior.CallActivityBehavior;
import org.activiti.engine.impl.bpmn.parser.BpmnParse;
import org.activiti.engine.impl.bpmn.parser.ErrorEventDefinition;
import org.activiti.engine.impl.bpmn.parser.EventSubscriptionDeclaration;
import org.activiti.engine.impl.jobexecutor.TimerDeclarationImpl;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.activiti.engine.impl.pvm.PvmTransition;
import org.activiti.engine.impl.pvm.delegate.ActivityBehavior;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.impl.pvm.process.Lane;
import org.activiti.engine.impl.pvm.process.LaneSet;
import org.activiti.engine.impl.pvm.process.ParticipantProcess;
import org.activiti.engine.impl.pvm.process.TransitionImpl;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.Execution;
import org.activiti.engine.runtime.ProcessInstance;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class BaseProcessDefinitionDiagramLayoutResource {

	@Autowired
	private RuntimeService runtimeService;

	@Autowired
	private RepositoryService repositoryService;

	@Autowired
	private HistoryService historyService;

	public ObjectNode getDiagramNode(String processInstanceId, String processDefinitionId) {

		List<String> highLightedFlows = Collections.<String> emptyList();
		List<String> highLightedActivities = Collections.<String> emptyList();

		Map<String, ObjectNode> subProcessInstanceMap = new HashMap<String, ObjectNode>();

		ProcessInstance processInstance = null;
		if (processInstanceId != null) {
			processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
			if (processInstance == null) {
				throw new ActivitiObjectNotFoundException("Process instance could not be found");
			}
			processDefinitionId = processInstance.getProcessDefinitionId();

			List<ProcessInstance> subProcessInstances = runtimeService.createProcessInstanceQuery().superProcessInstanceId(processInstanceId).list();

			for (ProcessInstance subProcessInstance : subProcessInstances) {
				String subDefId = subProcessInstance.getProcessDefinitionId();

				String superExecutionId = ((ExecutionEntity) subProcessInstance).getSuperExecutionId();
				ProcessDefinitionEntity subDef = (ProcessDefinitionEntity) repositoryService.getProcessDefinition(subDefId);

				ObjectNode processInstanceJSON = new ObjectMapper().createObjectNode();
				processInstanceJSON.put("processInstanceId", subProcessInstance.getId());
				processInstanceJSON.put("superExecutionId", superExecutionId);
				processInstanceJSON.put("processDefinitionId", subDef.getId());
				processInstanceJSON.put("processDefinitionKey", subDef.getKey());
				processInstanceJSON.put("processDefinitionName", subDef.getName());

				subProcessInstanceMap.put(superExecutionId, processInstanceJSON);
			}
		}

		if (processDefinitionId == null) {
			throw new ActivitiObjectNotFoundException("No process definition id provided");
		}

		ProcessDefinitionEntity processDefinition = (ProcessDefinitionEntity) repositoryService.getProcessDefinition(processDefinitionId);

		if (processDefinition == null) {
			throw new ActivitiException("Process definition " + processDefinitionId + " could not be found");
		}

		ObjectNode responseJSON = new ObjectMapper().createObjectNode();

		// Process definition
		JsonNode pdrJSON = getProcessDefinitionResponse(processDefinition);

		if (pdrJSON != null) {
			responseJSON.put("processDefinition", pdrJSON);
		}

		// Highlighted activities
		if (processInstance != null) {
			ArrayNode activityArray = new ObjectMapper().createArrayNode();
			ArrayNode flowsArray = new ObjectMapper().createArrayNode();

			highLightedActivities = runtimeService.getActiveActivityIds(processInstanceId);
			highLightedFlows = getHighLightedFlows(processInstanceId, processDefinition);

			for (String activityName : highLightedActivities) {
				activityArray.add(activityName);
			}

			for (String flow : highLightedFlows)
				flowsArray.add(flow);

			responseJSON.put("highLightedActivities", activityArray);
			responseJSON.put("highLightedFlows", flowsArray);
		}

		// Pool shape, if process is participant in collaboration
		if (processDefinition.getParticipantProcess() != null) {
			ParticipantProcess pProc = processDefinition.getParticipantProcess();

			ObjectNode participantProcessJSON = new ObjectMapper().createObjectNode();
			participantProcessJSON.put("id", pProc.getId());
			if (StringUtils.isNotEmpty(pProc.getName())) {
				participantProcessJSON.put("name", pProc.getName());
			} else {
				participantProcessJSON.put("name", "");
			}
			participantProcessJSON.put("x", pProc.getX());
			participantProcessJSON.put("y", pProc.getY());
			participantProcessJSON.put("width", pProc.getWidth());
			participantProcessJSON.put("height", pProc.getHeight());

			responseJSON.put("participantProcess", participantProcessJSON);
		}

		// Draw lanes

		if (processDefinition.getLaneSets() != null && !processDefinition.getLaneSets().isEmpty()) {
			ArrayNode laneSetArray = new ObjectMapper().createArrayNode();
			for (LaneSet laneSet : processDefinition.getLaneSets()) {
				ArrayNode laneArray = new ObjectMapper().createArrayNode();
				if (laneSet.getLanes() != null && !laneSet.getLanes().isEmpty()) {
					for (Lane lane : laneSet.getLanes()) {
						ObjectNode laneJSON = new ObjectMapper().createObjectNode();
						laneJSON.put("id", lane.getId());
						if (StringUtils.isNotEmpty(lane.getName())) {
							laneJSON.put("name", lane.getName());
						} else {
							laneJSON.put("name", "");
						}
						laneJSON.put("x", lane.getX());
						laneJSON.put("y", lane.getY());
						laneJSON.put("width", lane.getWidth());
						laneJSON.put("height", lane.getHeight());

						List<String> flowNodeIds = lane.getFlowNodeIds();
						ArrayNode flowNodeIdsArray = new ObjectMapper().createArrayNode();
						for (String flowNodeId : flowNodeIds) {
							flowNodeIdsArray.add(flowNodeId);
						}
						laneJSON.put("flowNodeIds", flowNodeIdsArray);

						laneArray.add(laneJSON);
					}
				}
				ObjectNode laneSetJSON = new ObjectMapper().createObjectNode();
				laneSetJSON.put("id", laneSet.getId());
				if (StringUtils.isNotEmpty(laneSet.getName())) {
					laneSetJSON.put("name", laneSet.getName());
				} else {
					laneSetJSON.put("name", "");
				}
				laneSetJSON.put("lanes", laneArray);

				laneSetArray.add(laneSetJSON);
			}

			if (laneSetArray.size() > 0)
				responseJSON.put("laneSets", laneSetArray);
		}

		ArrayNode sequenceFlowArray = new ObjectMapper().createArrayNode();
		ArrayNode activityArray = new ObjectMapper().createArrayNode();

		// Activities and their sequence-flows

		for (ActivityImpl activity : processDefinition.getActivities()) {
			getActivity(processInstanceId, activity, activityArray, sequenceFlowArray, processInstance, highLightedFlows, subProcessInstanceMap);
		}

		responseJSON.put("activities", activityArray);
		responseJSON.put("sequenceFlows", sequenceFlowArray);

		return responseJSON;
	}

	private List<String> getHighLightedFlows(String processInstanceId, ProcessDefinitionEntity processDefinition) {

		List<String> highLightedFlows = new ArrayList<String>();
		List<HistoricActivityInstance> historicActivityInstances = historyService.createHistoricActivityInstanceQuery()
				.processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();

		List<String> historicActivityInstanceList = new ArrayList<String>();
		for (HistoricActivityInstance hai : historicActivityInstances) {
			historicActivityInstanceList.add(hai.getActivityId());
		}

		// add current activities to list
		List<String> highLightedActivities = runtimeService.getActiveActivityIds(processInstanceId);
		historicActivityInstanceList.addAll(highLightedActivities);

		// activities and their sequence-flows
		for (ActivityImpl activity : processDefinition.getActivities()) {
			int index = historicActivityInstanceList.indexOf(activity.getId());

			if (index >= 0 && index + 1 < historicActivityInstanceList.size()) {
				List<PvmTransition> pvmTransitionList = activity.getOutgoingTransitions();
				for (PvmTransition pvmTransition : pvmTransitionList) {
					String destinationFlowId = pvmTransition.getDestination().getId();
					if (destinationFlowId.equals(historicActivityInstanceList.get(index + 1))) {
						highLightedFlows.add(pvmTransition.getId());
					}
				}
			}
		}
		return highLightedFlows;
	}

	private void getActivity(String processInstanceId, ActivityImpl activity, ArrayNode activityArray, ArrayNode sequenceFlowArray,
			ProcessInstance processInstance, List<String> highLightedFlows, Map<String, ObjectNode> subProcessInstanceMap) {

		ObjectNode activityJSON = new ObjectMapper().createObjectNode();

		// Gather info on the multi instance marker
		String multiInstance = (String) activity.getProperty("multiInstance");
		if (multiInstance != null) {
			if (!"sequential".equals(multiInstance)) {
				multiInstance = "parallel";
			}
		}

		ActivityBehavior activityBehavior = activity.getActivityBehavior();
		// Gather info on the collapsed marker
		Boolean collapsed = (activityBehavior instanceof CallActivityBehavior);
		Boolean expanded = (Boolean) activity.getProperty(BpmnParse.PROPERTYNAME_ISEXPANDED);
		if (expanded != null) {
			collapsed = !expanded;
		}

		Boolean isInterrupting = null;
		if (activityBehavior instanceof BoundaryEventActivityBehavior) {
			isInterrupting = ((BoundaryEventActivityBehavior) activityBehavior).isInterrupting();
		}

		// Outgoing transitions of activity
		for (PvmTransition sequenceFlow : activity.getOutgoingTransitions()) {
			String flowName = (String) sequenceFlow.getProperty("name");
			boolean isHighLighted = (highLightedFlows.contains(sequenceFlow.getId()));
			boolean isConditional = sequenceFlow.getProperty(BpmnParse.PROPERTYNAME_CONDITION) != null
					&& !((String) activity.getProperty("type")).toLowerCase().contains("gateway");
			boolean isDefault = sequenceFlow.getId().equals(activity.getProperty("default"))
					&& ((String) activity.getProperty("type")).toLowerCase().contains("gateway");

			List<Integer> waypoints = ((TransitionImpl) sequenceFlow).getWaypoints();
			ArrayNode xPointArray = new ObjectMapper().createArrayNode();
			ArrayNode yPointArray = new ObjectMapper().createArrayNode();
			for (int i = 0; i < waypoints.size(); i += 2) { // waypoints.size()
															// minimally 4: x1, y1,
															// x2, y2
				xPointArray.add(waypoints.get(i));
				yPointArray.add(waypoints.get(i + 1));
			}

			ObjectNode flowJSON = new ObjectMapper().createObjectNode();
			flowJSON.put("id", sequenceFlow.getId());
			flowJSON.put("name", flowName);
			flowJSON.put("flow", "(" + sequenceFlow.getSource().getId() + ")--" + sequenceFlow.getId() + "-->("
					+ sequenceFlow.getDestination().getId() + ")");

			if (isConditional)
				flowJSON.put("isConditional", isConditional);
			if (isDefault)
				flowJSON.put("isDefault", isDefault);
			if (isHighLighted)
				flowJSON.put("isHighLighted", isHighLighted);

			flowJSON.put("xPointArray", xPointArray);
			flowJSON.put("yPointArray", yPointArray);

			sequenceFlowArray.add(flowJSON);
		}

		// Nested activities (boundary events)
		ArrayNode nestedActivityArray = new ObjectMapper().createArrayNode();
		for (ActivityImpl nestedActivity : activity.getActivities()) {
			nestedActivityArray.add(nestedActivity.getId());
		}

		Map<String, Object> properties = activity.getProperties();
		ObjectNode propertiesJSON = new ObjectMapper().createObjectNode();
		for (String key : properties.keySet()) {
			Object prop = properties.get(key);
			if (prop instanceof String)
				propertiesJSON.put(key, (String) properties.get(key));
			else if (prop instanceof Integer)
				propertiesJSON.put(key, (Integer) properties.get(key));
			else if (prop instanceof Boolean)
				propertiesJSON.put(key, (Boolean) properties.get(key));
			else if ("initial".equals(key)) {
				ActivityImpl act = (ActivityImpl) properties.get(key);
				propertiesJSON.put(key, act.getId());
			} else if ("timerDeclarations".equals(key)) {
				ArrayList<TimerDeclarationImpl> timerDeclarations = (ArrayList<TimerDeclarationImpl>) properties.get(key);
				ArrayNode timerDeclarationArray = new ObjectMapper().createArrayNode();

				if (timerDeclarations != null)
					for (TimerDeclarationImpl timerDeclaration : timerDeclarations) {
						ObjectNode timerDeclarationJSON = new ObjectMapper().createObjectNode();

						timerDeclarationJSON.put("isExclusive", timerDeclaration.isExclusive());
						if (timerDeclaration.getRepeat() != null)
							timerDeclarationJSON.put("repeat", timerDeclaration.getRepeat());

						timerDeclarationJSON.put("retries", String.valueOf(timerDeclaration.getRetries()));
						timerDeclarationJSON.put("type", timerDeclaration.getJobHandlerType());
						timerDeclarationJSON.put("configuration", timerDeclaration.getJobHandlerConfiguration());
						//timerDeclarationJSON.put("expression", timerDeclaration.getDescription());

						timerDeclarationArray.add(timerDeclarationJSON);
					}
				if (timerDeclarationArray.size() > 0)
					propertiesJSON.put(key, timerDeclarationArray);
				// TODO: implement getting description
			} else if ("eventDefinitions".equals(key)) {
				ArrayList<EventSubscriptionDeclaration> eventDefinitions = (ArrayList<EventSubscriptionDeclaration>) properties.get(key);
				ArrayNode eventDefinitionsArray = new ObjectMapper().createArrayNode();

				if (eventDefinitions != null) {
					for (EventSubscriptionDeclaration eventDefinition : eventDefinitions) {
						ObjectNode eventDefinitionJSON = new ObjectMapper().createObjectNode();

						if (eventDefinition.getActivityId() != null)
							eventDefinitionJSON.put("activityId", eventDefinition.getActivityId());

						eventDefinitionJSON.put("eventName", eventDefinition.getEventName());
						eventDefinitionJSON.put("eventType", eventDefinition.getEventType());
						eventDefinitionJSON.put("isAsync", eventDefinition.isAsync());
						eventDefinitionJSON.put("isStartEvent", eventDefinition.isStartEvent());
						eventDefinitionsArray.add(eventDefinitionJSON);
					}
				}

				if (eventDefinitionsArray.size() > 0)
					propertiesJSON.put(key, eventDefinitionsArray);

				// TODO: implement it
			} else if ("errorEventDefinitions".equals(key)) {
				ArrayList<ErrorEventDefinition> errorEventDefinitions = (ArrayList<ErrorEventDefinition>) properties.get(key);
				ArrayNode errorEventDefinitionsArray = new ObjectMapper().createArrayNode();

				if (errorEventDefinitions != null) {
					for (ErrorEventDefinition errorEventDefinition : errorEventDefinitions) {
						ObjectNode errorEventDefinitionJSON = new ObjectMapper().createObjectNode();

						if (errorEventDefinition.getErrorCode() != null)
							errorEventDefinitionJSON.put("errorCode", errorEventDefinition.getErrorCode());
						else
							errorEventDefinitionJSON.putNull("errorCode");

						errorEventDefinitionJSON.put("handlerActivityId", errorEventDefinition.getHandlerActivityId());

						errorEventDefinitionsArray.add(errorEventDefinitionJSON);
					}
				}

				if (errorEventDefinitionsArray.size() > 0)
					propertiesJSON.put(key, errorEventDefinitionsArray);
			}

		}

		if ("callActivity".equals(properties.get("type"))) {
			CallActivityBehavior callActivityBehavior = null;

			if (activityBehavior instanceof CallActivityBehavior) {
				callActivityBehavior = (CallActivityBehavior) activityBehavior;
			}

			if (callActivityBehavior != null) {
				propertiesJSON.put("processDefinitonKey", callActivityBehavior.getProcessDefinitonKey());

				// get processDefinitonId from execution or get last processDefinitonId
				// by key
				ArrayNode processInstanceArray = new ObjectMapper().createArrayNode();
				if (processInstance != null) {
					List<Execution> executionList = runtimeService.createExecutionQuery().processInstanceId(processInstanceId)
							.activityId(activity.getId()).list();
					if (!executionList.isEmpty()) {
						for (Execution execution : executionList) {
							ObjectNode processInstanceJSON = subProcessInstanceMap.get(execution.getId());
							processInstanceArray.add(processInstanceJSON);
						}
					}
				}

				// If active activities nas no instance of this callActivity then add
				// last definition
				if (processInstanceArray.size() == 0 && StringUtils.isNotEmpty(callActivityBehavior.getProcessDefinitonKey())) {
					// Get last definition by key
					ProcessDefinition lastProcessDefinition = repositoryService.createProcessDefinitionQuery()
							.processDefinitionKey(callActivityBehavior.getProcessDefinitonKey()).latestVersion().singleResult();

					// TODO: unuseful fields there are processDefinitionName, processDefinitionKey
					if (lastProcessDefinition != null) {
						ObjectNode processInstanceJSON = new ObjectMapper().createObjectNode();
						processInstanceJSON.put("processDefinitionId", lastProcessDefinition.getId());
						processInstanceJSON.put("processDefinitionKey", lastProcessDefinition.getKey());
						processInstanceJSON.put("processDefinitionName", lastProcessDefinition.getName());
						processInstanceArray.add(processInstanceJSON);
					}
				}

				if (processInstanceArray.size() > 0) {
					propertiesJSON.put("processDefinitons", processInstanceArray);
				}
			}
		}

		activityJSON.put("activityId", activity.getId());
		activityJSON.put("properties", propertiesJSON);
		if (multiInstance != null)
			activityJSON.put("multiInstance", multiInstance);
		if (collapsed)
			activityJSON.put("collapsed", collapsed);
		if (nestedActivityArray.size() > 0)
			activityJSON.put("nestedActivities", nestedActivityArray);
		if (isInterrupting != null)
			activityJSON.put("isInterrupting", isInterrupting);

		activityJSON.put("x", activity.getX());
		activityJSON.put("y", activity.getY());
		activityJSON.put("width", activity.getWidth());
		activityJSON.put("height", activity.getHeight());

		activityArray.add(activityJSON);

		// Nested activities (boundary events)
		for (ActivityImpl nestedActivity : activity.getActivities()) {
			getActivity(processInstanceId, nestedActivity, activityArray, sequenceFlowArray, processInstance, highLightedFlows, subProcessInstanceMap);
		}
	}

	private JsonNode getProcessDefinitionResponse(ProcessDefinitionEntity processDefinition) {
		ObjectMapper mapper = new ObjectMapper();
		ObjectNode pdrJSON = mapper.createObjectNode();
		pdrJSON.put("id", processDefinition.getId());
		pdrJSON.put("name", processDefinition.getName());
		pdrJSON.put("key", processDefinition.getKey());
		pdrJSON.put("version", processDefinition.getVersion());
		pdrJSON.put("deploymentId", processDefinition.getDeploymentId());
		pdrJSON.put("isGraphicNotationDefined", isGraphicNotationDefined(processDefinition));
		return pdrJSON;
	}

	private boolean isGraphicNotationDefined(ProcessDefinitionEntity processDefinition) {
		return ((ProcessDefinitionEntity) repositoryService.getProcessDefinition(processDefinition.getId())).isGraphicalNotationDefined();
	}
}
